iT邦幫忙

2023 iThome 鐵人賽

DAY 17
0
DevOps

大家都在用 Terraform 實作 IaC 為什麼不將程式寫得更簡潔易讀呢?系列 第 17

實作 AWS 常用服務之 Terraform 模組系列 - CloudFront 篇

  • 分享至 

  • xImage
  •  

AWS S3 模組實作

本篇是實作常用的 AWS CloudFront 服務之 Terraform 模組,並且會使用到 YAML 資料結構來定義模組的內容,完整的專案程式碼分享在我的 Github 上。

  1. 先定義整個專案檔案結構設定檔 ./configs/cloudfront/distributions.yaml 與 模組 my_cloudfront 的放置位置 modules/my_cloudfront:
├── configs
│   ├── iam
│   │   ├── assume_role_policies
│   │   ├── policies
│   │   ├── role_policies
│   │   ├── user_policies
│   │   └── iam.yaml
│   ├── cloudfront
│   │   └── distributions.yaml
│   ├── s3
│   │   ├── policies
│   │   └── s3.yaml
│   ├── subnet
│   │   └── my-subnets.yaml
│   └── vpc
│       └── my-vpcs.yaml
├── example.tfvars
├── locals.tf
├── main.tf
├── modules
│   ├── my_cloudfront
│   │   ├── aws_cloudfront_response_headers_policy.tf
│   │   ├── cloudfront_distribution.tf
│   │   ├── outputs.tf
│   │   ├── provider.tf
│   │   └── variables.tf
│   ├── my_eips
│   ├── my_eips
│   ├── my_iam
│   ├── my_igw
│   ├── my_instances
│   ├── my_nacls
│   ├── my_route_tables
│   ├── my_s3
│   ├── my_subnets
│   └── my_vpc
└── variables.tf
  1. 撰寫 ./configs/cloudfront/distributions.yaml 內容來定義 CloudFront 需要用建立的資源:
distributions:
  - alias: "<ALIAS_NAME>"
    department: "<DEPARTMENT_NAME>"
    project: "<PROJECT_NAME>"
    bucket_name: "<BUCKET_NAME>"
    origin_path: ""
    logging_bucket: ""
    logging_prefix: ""
    response_headers_policy: "<RESPONSE_HEADERS_POLICY_NAME>"
    custom_error_response:
      - error_caching_min_ttl: "10"
        error_code: "403"
        response_code: "200"
        response_page_path: "/index.html"
      - error_caching_min_ttl: "10"
        error_code: "404"
        response_code: "200"
        response_page_path: "/index.html"

response_headers_policies:
  - name: "<CUSTOM_SECURITY_HEADERS_POLICY_NAME>"
    custom_headers_configs:
      - header: "Cache-Control"
        override: true
        value: "no-store"
    security_headers_config:
      content_security_policy:
        content_security_policy: "<CONTENT_SECURITY_POLICY>"
        override: true
      content_type_options:
        override: true
      frame_options:
        frame_option: "SAMEORIGIN"
        override: true
      referrer_policy:
        override: true
        referrer_policy: "strict-origin-when-cross-origin"
      strict_transport_security:
        access_control_max_age_sec: 31536000
        include_subdomains: true
        override: true
        preload: true

  1. 撰寫 my_cloudfront 模組:
  • ./modules/my_cloudfront/outputs.tf:
output "distributions" {
  value = aws_cloudfront_distribution.distributions
}
  • ./modules/my_cloudfront/provider.tf:
provider "aws" {
    region  = var.aws_region
    profile = var.aws_profile
}
  • ./modules/my_cloudfront/variables.tf:
variable "aws_region" {
  description = "AWS region"
  default     = "ap-northeast-1"
}

variable "aws_profile" {
  description = "AWS profile"
  default     = ""
}

variable "environment" {
  type    = string
  default = ""
}

variable "access_identity_path" {
  type    = string
  default = ""
}

variable "acm_certificate_arn" {
  type    = string
  default = ""
}

variable "distribution_path" {
  type    = string
  default = ""
}

variable "s3_buckets" {
  type    = map(any)
  default = {}
}

  • ./modules/my_cloudfront/cloudfront_distribution.tf:
locals {
  distributions = yamldecode(file("${var.distribution_path}"))["distributions"]
}

resource "aws_cloudfront_distribution" "distributions" {
  for_each = { for r in local.distributions : r.name => r }

  aliases = each.value.aliases

  dynamic "custom_error_response" {

    for_each = lookup(each.value, "custom_error_response", [])

    content {
      error_caching_min_ttl = custom_error_response.value.error_caching_min_ttl
      error_code            = custom_error_response.value.error_code
      response_code         = custom_error_response.value.response_code
      response_page_path    = custom_error_response.value.response_page_path
    }
  }

  default_cache_behavior {
    allowed_methods            = ["GET", "HEAD"]
    cache_policy_id            = "658327ea-f89d-4fab-a63d-7e88639e58f6"
    cached_methods             = ["GET", "HEAD"]
    compress                   = "true"
    default_ttl                = "0"
    max_ttl                    = "0"
    min_ttl                    = "0"
    smooth_streaming           = "false"
    target_origin_id           = "S3-${format("%s%s", each.value.bucket_name, each.value.origin_path)}"
    viewer_protocol_policy     = "redirect-to-https"
    response_headers_policy_id = (each.value.response_headers_policy != "") ? aws_cloudfront_response_headers_policy.policies["${each.value.response_headers_policy}"].id : null

    dynamic "lambda_function_association" {
      for_each = lookup(each.value, "lambda_function_association", null) == null ? [] : [1]
      content {
        event_type   = each.value.lambda_function_association.event_type
        include_body = each.value.lambda_function_association.include_body
        lambda_arn   = each.value.lambda_function_association.lambda_arn
      }
    }
  }

  enabled         = "true"
  http_version    = "http2"
  is_ipv6_enabled = "true"

  dynamic "logging_config" {
    for_each = each.value.logging_bucket == "" ? [] : [1]
    content {
      bucket          = "${each.value.logging_bucket}.s3.amazonaws.com"
      include_cookies = "false"
      prefix          = each.value.logging_prefix
    }
  }

  origin {
    connection_attempts = "3"
    connection_timeout  = "10"
    domain_name         = "${each.value.bucket_name}.s3.amazonaws.com"
    origin_id           = "S3-${format("%s%s", each.value.bucket_name, each.value.origin_path)}"
    origin_path         = each.value.origin_path != "" ? each.value.origin_path : null

    s3_origin_config {
      origin_access_identity = var.access_identity_path
    }
  }

  price_class = "PriceClass_All"

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  retain_on_delete = "false"

  tags = {
    Department = each.value.department
    Env        = var.environment
    Name       = "${format("%s%s", each.value.bucket_name, each.value.origin_path)}-cf"
    Project    = each.value.project
  }

  tags_all = {
    Department = each.value.department
    Env        = var.environment
    Name       = "${format("%s%s", each.value.bucket_name, each.value.origin_path)}-cf"
    Project    = each.value.project
  }

  viewer_certificate {
    acm_certificate_arn            = var.acm_certificate_arn
    cloudfront_default_certificate = "false"
    minimum_protocol_version       = "TLSv1.2_2021"
    ssl_support_method             = "sni-only"
  }

  depends_on = [
    aws_cloudfront_response_headers_policy.policies,
    var.s3_buckets
  ]
}

  • ./modules/my_cloudfront/aws_cloudfront_response_headers_policy.tf:
locals {
  response_headers_policies = yamldecode(file("${var.distribution_path}"))["response_headers_policies"]
}

resource "aws_cloudfront_response_headers_policy" "policies" {

  for_each = { for r in local.response_headers_policies : r.name => r }

  name = each.value.name

  dynamic "custom_headers_config" {
    for_each = lookup(each.value, "custom_headers_configs", null) != null ? [1] : []

    content {
      dynamic "items" {
        for_each = each.value.custom_headers_configs

        content {
          header   = items.value["header"]
          override = items.value["override"]
          value    = items.value["value"]
        }
      }
    }
  }

  dynamic "security_headers_config" {
    for_each = lookup(each.value, "security_headers_config", null) != null ? [each.value.security_headers_config] : []

    content {
      dynamic "content_security_policy" {
        for_each = lookup(security_headers_config.value, "content_security_policy", null) != null ? [security_headers_config.value["content_security_policy"]] : []

        content {
          content_security_policy = content_security_policy.value["content_security_policy"]
          override                = content_security_policy.value["override"]
        }
      }

      dynamic "content_type_options" {
        for_each = lookup(security_headers_config.value, "content_type_options", null) != null ? [security_headers_config.value["content_type_options"]] : []

        content {
          override = content_type_options.value["override"]
        }
      }

      dynamic "frame_options" {
        for_each = lookup(security_headers_config.value, "frame_options", null) != null ? [security_headers_config.value["frame_options"]] : []

        content {
          frame_option = frame_options.value["frame_option"]
          override     = frame_options.value["override"]
        }
      }

      dynamic "referrer_policy" {
        for_each = lookup(security_headers_config.value, "referrer_policy", null) != null ? [security_headers_config.value["referrer_policy"]] : []

        content {
          override        = referrer_policy.value["override"]
          referrer_policy = referrer_policy.value["referrer_policy"]
        }
      }

      dynamic "strict_transport_security" {
        for_each = lookup(security_headers_config.value, "strict_transport_security", null) != null ? [security_headers_config.value["strict_transport_security"]] : []

        content {
          access_control_max_age_sec = strict_transport_security.value["access_control_max_age_sec"]
          include_subdomains         = strict_transport_security.value["include_subdomains"]
          override                   = strict_transport_security.value["override"]
          preload                    = strict_transport_security.value["preload"]
        }
      }
    }
  }
}

  1. 撰寫專案相關程式
  • example.tfvars:
aws_region="ap-northeast-1"
aws_profile="<YOUR_PROFILE>"
project_name="example"
department_name="SRE"
  • main.tf:
terraform {
  required_providers {
    aws = {
      version = "5.15.0"
    }
  }

  backend "s3" {
    bucket                  = "<YOUR_S3_BUCKET_NAME>"
    dynamodb_table          = "<YOUR_DYNAMODB_TABLE_NAME>"
    key                     = "terraform.tfstate"
    region                  = "ap-northeast-1"
    shared_credentials_file = "~/.aws/config"
    profile                 = "<YOUR_PROFILE>"
  }
}

# 其他模組省略...

# CloudFront 需要搭配與 S3 模組一起使用
# s3
module "s3" {
  aws_profile = var.aws_profile
  aws_region  = var.aws_region
  environment = var.environment
  s3_path     = "./configs/s3/s3.yaml"

  source = "./modules/my_s3"
}

# cloudfront
module "cloudfront" {
  aws_profile          = var.aws_profile
  aws_region           = var.aws_region
  acm_certificate_arn  = "arn:aws:acm:us-east-1:188714232447:certificate/66dd6c2a-3482-474c-9dbb-befb925a0147"
  access_identity_path = "origin-access-identity/cloudfront/XXXXXXXXXXXXXX"
  distribution_path    = "./configs/cloudfront/distributions.yaml"
  s3_buckets           = module.s3.s3_bucket

  source = "./modules/my_cloudfront"
}


Terraform 執行計畫

  1. 嘗試建立一個 CloudFront Distribution 來測試一下模組:
distributions:
  - name: my-cloudfront
    aliases:
      - my-cloudfront.nextdrive.io
    department: SRE
    project: EXAMPLE
    bucket_name: my-bucket
    origin_path: ""
    logging_bucket: ""
    logging_prefix: ""
    response_headers_policy: "NoStoreResponseHeader"
    custom_error_response:
      - error_caching_min_ttl: "10"
        error_code: "403"
        response_code: "200"
        response_page_path: "/index.html"
      - error_caching_min_ttl: "10"
        error_code: "404"
        response_code: "200"
        response_page_path: "/index.html"

response_headers_policies:
  - name: "NoStoreResponseHeaderWithCORS"
    cors_config:
      access_control_allow_credentials: false
      access_control_allow_headers:
        - "*"
      access_control_allow_methods:
        - "DELETE"
        - "GET"
        - "HEAD"
        - "OPTIONS"
        - "PATCH"
        - "POST"
        - "PUT"
      access_control_allow_origins:
        - "http://127.0.0.1:3000"
      access_control_max_age_sec: 31536000
      origin_override: true
    custom_headers_configs:
      - header: "Cache-Control"
        override: true
        value: "no-store"

  1. 於專案目錄下執行 terraform init && terraform plan --out .plan -var-file=example.tfvars 來確認一下結果:

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following
symbols:
  + create

Terraform will perform the following actions:

  # module.cloudfront.aws_cloudfront_distribution.distributions["my-cloudfront"] will be created
  + resource "aws_cloudfront_distribution" "distributions" {
      + aliases                        = [
          + "my-cloudfront.nextdrive.io",
        ]
      + arn                            = (known after apply)
      + caller_reference               = (known after apply)
      + domain_name                    = (known after apply)
      + enabled                        = true
      + etag                           = (known after apply)
      + hosted_zone_id                 = (known after apply)
      + http_version                   = "http2"
      + id                             = (known after apply)
      + in_progress_validation_batches = (known after apply)
      + is_ipv6_enabled                = true
      + last_modified_time             = (known after apply)
      + price_class                    = "PriceClass_All"
      + retain_on_delete               = false
      + staging                        = false
      + status                         = (known after apply)
      + tags                           = {
          + "Department" = "SRE"
          + "Env"        = ""
          + "Name"       = "my-bucket-cf"
          + "Project"    = "EXAMPLE"
        }
      + tags_all                       = (known after apply)
      + trusted_key_groups             = (known after apply)
      + trusted_signers                = (known after apply)
      + wait_for_deployment            = true

      + custom_error_response {
          + error_caching_min_ttl = 10
          + error_code            = 403
          + response_code         = 200
          + response_page_path    = "/index.html"
        }
      + custom_error_response {
          + error_caching_min_ttl = 10
          + error_code            = 404
          + response_code         = 200
          + response_page_path    = "/index.html"
        }

      + default_cache_behavior {
          + allowed_methods            = [
              + "GET",
              + "HEAD",
            ]
          + cache_policy_id            = "658327ea-f89d-4fab-a63d-7e88639e58f6"
          + cached_methods             = [
              + "GET",
              + "HEAD",
            ]
          + compress                   = true
          + default_ttl                = 0
          + max_ttl                    = 0
          + min_ttl                    = 0
          + response_headers_policy_id = (known after apply)
          + smooth_streaming           = false
          + target_origin_id           = "S3-my-bucket"
          + trusted_key_groups         = (known after apply)
          + trusted_signers            = (known after apply)
          + viewer_protocol_policy     = "redirect-to-https"
        }

      + origin {
          + connection_attempts = 3
          + connection_timeout  = 10
          + domain_name         = "my-bucket.s3.amazonaws.com"
          + origin_id           = "S3-my-bucket"

          + s3_origin_config {
              + origin_access_identity = "origin-access-identity/cloudfront/XXXXXXXXXXXXXX"
            }
        }

      + restrictions {
          + geo_restriction {
              + locations        = (known after apply)
              + restriction_type = "none"
            }
        }

      + viewer_certificate {
          + acm_certificate_arn            = "arn:aws:acm:us-east-1:188714232447:certificate/66dd6c2a-3482-474c-9dbb-befb925a0147"
          + cloudfront_default_certificate = false
          + minimum_protocol_version       = "TLSv1.2_2021"
          + ssl_support_method             = "sni-only"
        }
    }

  # module.cloudfront.aws_cloudfront_response_headers_policy.policies["NoStoreResponseHeaderWithCORS"] will be created
  + resource "aws_cloudfront_response_headers_policy" "policies" {
      + etag = (known after apply)
      + id   = (known after apply)
      + name = "NoStoreResponseHeaderWithCORS"

      + cors_config {
          + access_control_allow_credentials = false
          + access_control_max_age_sec       = 31536000
          + origin_override                  = true

          + access_control_allow_headers {
              + items = [
                  + "*",
                ]
            }

          + access_control_allow_methods {
              + items = [
                  + "DELETE",
                  + "GET",
                  + "HEAD",
                  + "OPTIONS",
                  + "PATCH",
                  + "POST",
                  + "PUT",
                ]
            }

          + access_control_allow_origins {
              + items = [
                  + "http://127.0.0.1:3000",
                ]
            }
        }

      + custom_headers_config {
          + items {
              + header   = "Cache-Control"
              + override = true
              + value    = "no-store"
            }
        }
    }

  # module.s3.aws_s3_bucket.s3_bucket["my-bucket"] will be created
  + resource "aws_s3_bucket" "s3_bucket" {
      + acceleration_status         = (known after apply)
      + acl                         = (known after apply)
      + arn                         = (known after apply)
      + bucket                      = "my-bucket"
      + bucket_domain_name          = (known after apply)
      + bucket_prefix               = (known after apply)
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = false
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + object_lock_enabled         = (known after apply)
      + policy                      = (known after apply)
      + region                      = (known after apply)
      + request_payer               = (known after apply)
      + tags                        = {
          + "Department" = "The department of bucket"
          + "Name"       = "my-bucket"
          + "Project"    = ""
        }
      + tags_all                    = (known after apply)
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)
    }

  # module.s3.aws_s3_bucket_lifecycle_configuration.configurations["my-bucket"] will be created
  + resource "aws_s3_bucket_lifecycle_configuration" "configurations" {
      + bucket = (known after apply)
      + id     = (known after apply)

      + rule {
          + id     = "delete-index"
          + status = "Disabled"

          + expiration {
              + days                         = 3
              + expired_object_delete_marker = false
            }

          + filter {
              + prefix = "some-prefix/"
            }
        }
    }

  # module.s3.aws_s3_bucket_policy.s3_bucket_policy["my-bucket"] will be created
  + resource "aws_s3_bucket_policy" "s3_bucket_policy" {
      + bucket = "my-bucket"
      + id     = (known after apply)
      + policy = jsonencode(
            {
              + Id        = "PolicyForMyBucket"
              + Statement = []
              + Version   = "2008-10-17"
            }
        )
    }

  # module.s3.aws_s3_bucket_server_side_encryption_configuration.configurations["my-bucket"] will be created
  + resource "aws_s3_bucket_server_side_encryption_configuration" "configurations" {
      + bucket = (known after apply)
      + id     = (known after apply)

      + rule {
          + bucket_key_enabled = false

          + apply_server_side_encryption_by_default {
              + sse_algorithm = "AES256"
            }
        }
    }

  # module.s3.aws_s3_bucket_versioning.versionings["my-bucket"] will be created
  + resource "aws_s3_bucket_versioning" "versionings" {
      + bucket = (known after apply)
      + id     = (known after apply)

      + versioning_configuration {
          + mfa_delete = (known after apply)
          + status     = "Enabled"
        }
    }

Plan: 7 to add, 0 to change, 0 to destroy.
───────────────────────────────────────────────────────────────────────

Saved the plan to: .plan

To perform exactly these actions, run the following command to apply:
    terraform apply ".plan"
Releasing state lock. This may take a few moments...

下一篇文章將會展示實作 AWS CloudWatch 之 Terraform 模組。


上一篇
實作 AWS 常用服務之 Terraform 模組系列 - S3 篇
下一篇
實作 AWS 常用服務之 Terraform 模組系列 - CloudWatch LogGroup 和 EventBridge 篇
系列文
大家都在用 Terraform 實作 IaC 為什麼不將程式寫得更簡潔易讀呢?30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言